跳到主要内容

低级 API 用法(Low-level API Usage)

背景

虽然 JS 运行时提供了一个高层 API,可以快速将 Rive 集成到 Web 应用中,但运行时也提供了一个更小的高级低级 API,允许你在自己的渲染循环中构建和控制 Rive。有几个使用此低级 API 的原因和好处:

  • 在一个 <canvas> 元素中构建多个 Rive 文件、Artboards、Linear Animations 和 State Machines 的场景。这在构建游戏时非常有用!
  • 控制渲染循环,这涉及如何随着时间推进每个 artboard、animation 和 state machine(包括速度)
  • 能够访问绘制层级中节点/骨骼上的多个转换属性值
  • 更小的依赖体积
  • ……以及更多!

前提

以下是使用低级 API 在 Rive 上执行渲染的基本工作流程:

  1. 加载 Rive Web Assembly(WASM)文件(包含低级 API 模块)
  2. 加载 Rive 文件
  3. 为 Artboard、LinearAnimation 和 StateMachine 创建实例
  4. 构建渲染循环并驱动这些实例:
    • 推进动画实例并应用
    • 推进状态机实例
    • 推进 artboard
    • 将更新后的 artboard 渲染到 canvas
    • 请求下一帧
  5. 使用完成后清理创建的实例

开始

如果你已经决定低级 JS API 是你的应用所需,请继续阅读下面的指南了解如何完成所有设置,或直接跳到最后查看一些示例。

加载 WASM

设置低级 Rive API 的第一步是从 @rive-app/canvas-advanced@rive-app/webgl2-advanced 库加载 Rive WASM 文件(默认推荐 @rive-app/canvas-advanced,体积更小,除非你需要使用 WebGL2)。当 WASM 文件加载到你的应用后,你将获得访问必要 API 的权限,例如 Canvas/WebGL 的 Renderer,以及来自底层 rive-cpp(Rive 的 C++ 核心运行时)生成的相关 JS 类。你将使用这些类在下面的 canvas 中构建渲染场景。

你可以通过 unpkg(托管我们的 JS 运行时 NPM 模块)加载 Rive WASM 文件,这会向 CDN 发出网络请求,或者你也可以选择在自己的服务器上托管 WASM 文件。使用 unpkg 时,URL 看起来会像这样:

https://unpkg.com/@rive-app/[email protected]/rive.wasm

备注

你需要确保 @rive-app/canvas-advanced@@rive-app/webgl2-advanced@ 后面的版本号与你在应用中安装的依赖版本一致。例如,如果你在 package.json 中安装了 @rive-app/[email protected],那么你从 unpkg 请求的 Rive WASM 文件应该是 https://unpkg.com/@rive-app/[email protected]/rive.wasm。

查看 预加载 WASM 以提前加载 WASM。

首先,从库中导入默认模块,然后通过一个对象回调调用它,其中只需要设置一个参数 locateFile,该函数返回 WASM 文件的 URI。可以是 unpkg URL,也可以是你自托管的 URI。只需 await 调用完成,就会获得低级 Rive 运行时 API 的引用。

import RiveCanvas from '@rive-app/canvas-advanced';

async function main() {
const rive = await RiveCanvas({
locateFile: (_) => '<https://unpkg.com/@rive-app/[email protected]/rive.wasm>'
});
}
main();

创建 Renderer

WASM 加载完成后,下一步是通过 makeRenderer() API 创建 renderer,并传入用于渲染 Rive 的 canvas 元素。renderer 会使用渲染上下文将 Rive 绘制到 <canvas> 元素上。如果你使用的是 @rive-app/canvas-advanced,它会创建一个 Canvas2D 渲染上下文。如果你使用的是 @rive-app/webgl2-advanced,它会创建一个 WebGL 渲染上下文。

const canvas = document.getElementById('your-canvas-element');
const renderer = rive.makeRenderer(canvas);

加载 Rive 文件

创建 renderer 后,你还可以开始加载 Rive 文件(以 ArrayBuffer 形式),然后将其传入运行时的 load() API。你可以从 URL 获取,也可以在项目内部某处获取。

const bytes = await (
await fetch(new Request('basketball.riv'))
).arrayBuffer();

// import File as a named import from the Rive dependency
const file = (await rive.load(new Uint8Array(bytes))) as File;
备注

请确保等待 .load() 调用完成,因为它会同步地尝试从 File 中加载资源。此外,在将 ArrayBuffer 传递给 .load() 之前,应先对其创建一个 Uint8Array 视图。

设置实例

一旦你拿到加载完成的 File 对象的引用,就可以开始对 Rive 文件中的所有 artboards、state machines 和 linear animations 进行实例化。实例化会创建一个底层的 C++ 引用,允许你控制每个实体随时间的推进。稍后会在本指南中详细介绍。

你最可能要实例化的主要组件包括:

  • Artboard - 实例化 1 个或多个 artboard 来进行绘制
  • StateMachineInstance - 从某个 artboard 中实例化一个状态机
  • LinearAnimationInstance - 从某个 artboard 中实例化一个时间线动画

首先实例化一个 artboard,然后你可以从该 artboard 引用中创建状态机和线性动画实例,如下所示。

const artboard = file.artboardByName('New Artboard');
const animation = new rive.LinearAnimationInstnace(
artboard.animationByName('idle'),
artboard
);
const stateMachine = new rive.StateMachineInstance(
artboard.stateMachineByName('your-state-machine-name'),
artboard
);

好处之一是,如果你想在 canvas 中显示多个 artboards,甚至多个相同 artboard 的副本,这是非常容易完成的(而高层 API 只能一次展示一个)。

除了实例化用于渲染循环的相关部分之外,你还可以提取绘制层级中的节点、目标和骨骼引用。如果你需要在动画生命周期中跟踪某个节点的转换属性(换句话说,比如追踪某个节点的 x、y 坐标或旋转值),或者需要获取世界空间或父级转换,这会非常有用。请查看本指南底部的一些示例来查看实际操作。

构建渲染循环

你可能已经熟悉使用 requestAnimationFrame(rAF)构建渲染循环,以在浏览器重绘周期之间逐帧构建动画。如果不熟悉,可以先阅读 这篇指南 了解 JS 中如何构建主循环。

在 Rive 渲染循环中,你将使用一个自定义的 Rive API 来封装 rAF,因此需要使用 rive.requestAnimationFrame() 以及 rive.cancelAnimationFrame()。结构应该与其他动画项目中构建的 rAF 循环类似,但你需要在该循环中推进上述创建的实例,并根据需要调整 artboard 与 canvas 的对齐方式。

首先创建 rAF 回调循环,并记录前一个回调以来的时间,以便获取以秒为单位的经过时间。然后使用 renderer 的 .clear() API 清除画布。

let lastTime = 0;
function renderLoop(time) {
if (!lastTime) {
lastTime = time;
}
const elapsedTimeMs = time - lastTime;
const elapsedTimeSec = elapsedTimeMs / 1000;
lastTime = time;

renderer.clear();

...

rive.requestAnimationFrame(renderLoop);
}
rive.requestAnimationFrame(renderLoop);

推进动画

LinearAnimationInstance 拥有一组关键帧,用于应用到 artboard 中的对象。在渲染循环中,需要对创建的 animation 实例调用 .advance(),以获取这些关键帧,并按名称推进动画(一段时间,以秒为单位)。

备注

通常,你会将上述计算的经过时间用作推进动画的时间参数,以便以“正常”速度播放(或者说,以该时间线动画设置的速度播放)。通过低级 API,你可以按自定义时间推进实例,例如使用经过时间的一半以 0.5x 的速度播放,或是使用两倍的时间以 2x 的速度播放。你甚至可以把经过时间乘以 -1 来反向播放动画。

除了推进线性动画之外,你还需要使用 .apply() 将关键帧值应用到 artboard 中的相关对象属性上,并指定动画的混合值。当动画应用关键帧值时,它会将这些值与 artboard 上对象的当前值混合,这使你可以“混合”动画,尤其在两个动画实例为同一对象属性应用关键帧值时非常有用。默认的混合值为 1,表示用新关键帧值替换旧值。

在应用动画值之后,推进 artboard(稍后详细说明),以更新 artboard 对象并解决属性值变化。

总结上述流程,推进线性动画的操作顺序如下:

advance animation -> apply animation values -> advance artboard

以下代码展示了示例:

function renderLoop(time) {
if (!lastTime) {
lastTime = time;
}
const elapsedTimeMs = time - lastTime;
const elapsedTimeSec = elapsedTimeMs / 1000;
lastTime = time;

renderer.clear();
animation.advance(elapsedTimeSec);
animation.apply(1);
artboard.advance(elapsedTimeSec);
}

推进状态机

StateMachineInstance 的流程与 LinearAnimationInstance 类似,但有一些区别。对于状态机,无需传入混合值,因为通常只在与某个 artboard 关联的一个状态机实例上工作,混合值由状态之间的过渡决定。此外,.advance() 方法本身会更新 artboard 上对象的属性值。因此,推进状态机的操作顺序简化为:

advance state machine -> advance artboard

下面示例演示了这一流程:

function renderLoop(time) {
if (!lastTime) {
lastTime = time;
}
const elapsedTimeMs = time - lastTime;
const elapsedTimeSec = elapsedTimeMs / 1000;
lastTime = time;

renderer.clear();
stateMachine.advance(elapsedTimeSec);
artboard.advance(elapsedTimeSec);
}

推进 Artboard

如上所述,推进 artboard 会在动画或状态机应用值后更新层级中相关的对象。如果你同时控制多个动画,只需在渲染循环中推进 artboard 一次即可。如果你在画布中控制多个 artboard,请根据需要对每个 artboard 进行推进。

对齐与渲染

渲染循环中最后需要做的事情是设置 artboard 的对齐方式、绘制区域以及将渲染上下文传递给 artboard,从而让 artboard 绘制到 canvas 中。

在推进 artboard 后先调用 renderer 的 save() API,以保存 canvas 的状态。然后调用 context 的 align() API,传入:

  1. FitAlignment
  2. 要绘制的 canvas 空间的边界
  3. 要在该空间中绘制的 Rive 内容边界

在此处 查看 FitAlignment 的选项。对于后两个参数,提供一个轴对齐边界框(AABB)。下面的示例展示了 align() API 的用法。

最后,在调用 align() 后,通过 draw() 方法将 renderer 传递给 artboard 以在 canvas 上绘制 artboard,然后调用 renderer 的 restore() API 以恢复之前保存的 canvas 状态。

备注

如果你使用的是 @rive-app/webgl2-advanced,还需要调用 renderer.flush() 来清除不同的缓冲命令。

最后一步是在回调函数中再次调用 Rive 的 requestAnimationFrame,以排队下一帧。

整体如下:

function renderLoop(time) {
if (!lastTime) {
lastTime = time;
}
const elapsedTimeMs = time - lastTime;
const elapsedTimeSec = elapsedTimeMs / 1000;
lastTime = time;

...

renderer.clear();
stateMachine.advance(elapsedTimeSec);
artboard.advance(elapsedTimeSec);
renderer.save();
renderer.align(
rive.Fit.contain,
rive.Alignment.center,
{
minX: 0,
minY: 0,
maxX: canvas.width,
maxY: canvas.height
},
artboard.bounds,
);
artboard.draw(renderer);
renderer.restore();
// Optionally make the below call if using WebGL
// renderer.flush()
rive.requestAnimationFrame(renderLoop);
}
rive.requestAnimationFrame(renderLoop);

到此,你应该可以在 canvas 上渲染 Rive 了!

清理实例

对于每个创建的 C++ 实例,当不再使用时都要显式删除,以避免应用中出现内存泄漏。不幸的是,这是一个手动操作,因为我们不能依赖浏览器中的新 finalizer API 来触发垃圾回收。请在不需要这些由 Rive 运行时创建的实例时调用 .delete()。下面是示例:

// Created instances
const renderer = rive.makeRenderer(canvas);
const bytes = await (
await fetch(new Request('basketball.riv'))
).arrayBuffer();
const file = (await rive.load(new Uint8Array(bytes))) as File;
const artboard = file.artboardByName('New Artboard');
const animation = new rive.LinearAnimationInstnace(
artboard.animationByName('idle'),
artboard
);
const stateMachine = new rive.StateMachineInstance(
artboard.stateMachineByName('your-state-machine-name'),
artboard
);

...

renderer.delete();
file.delete();
artboard.delete();
animation.delete();
stateMachine.delete();

示例

下面是演示低级 JS API 的一些示例链接:

API 参考

请查看我们的 types 文件,了解高级 API 的签名与返回类型。

注意事项

高层 JS 运行时 API 是基于上文介绍的低层 API 构建的。同时,高层 JS 运行时还提供了许多便捷功能,帮助用户轻松完成以下事务:

  • 使用 .play().pause().stop() 等 API 控制播放
  • onStateChangeonLoad 等回调,以便钩入特定的 Rive 生命周期事件
  • 将手势事件连接到 Rive Listeners

使用 Rive 的高级 JS API 定制你的使用方式时,需要你自己实现其中一些便捷功能。如果有需要,可以查看 高层 Rive API 的构建方式,以了解如何复现一些高层功能。

将 Rive 集成到现有的 rAF 循环中

如果你想将 Rive 添加到现有的渲染循环(JS API requestAniationFrame()),而又不想使用 Rive 封装的 requestAnimationFrame() API,可以在渲染循环末尾额外调用一次 rive.resolveAnimationFrame()。在调用 requestAnimationFrame() 之前,先调用这一 API。

有关用法,可参见 Rive Parameters 文档。